/* Copyright (C) 1999-2005 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY 
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion;

import artofillusion.animation.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;

import java.awt.*;
import java.util.*;

/** The MeshEditorWindow class represents the window for editing Mesh objects.  This is an
    abstract class, with subclasses for various types of objects. */

public abstract class MeshEditorWindow extends BFrame implements MeshEditController, EditingWindow
{
  protected EditingWindow parentWindow;
  protected ObjectInfo objInfo;
  protected MeshViewer theView[];
  protected BorderContainer viewPanel[];
  protected GridContainer viewsContainer;
  protected int numViewsShown, currentView;
  protected ToolPalette tools;
  protected EditingTool defaultTool, currentTool;
  protected BLabel helpText;
  protected Mesh oldMesh;
  protected BMenuBar menubar;
  protected BMenu viewMenu;
  protected BMenuItem undoItem, redoItem, templateItem, axesItem, splitViewItem;
  protected BCheckBoxMenuItem displayItem[], coordsItem[], showItem[];
  protected UndoStack undoStack;

  protected static int meshTension = 2;
  protected static int tensionDistance = 0;
  protected static final double[] tensionArray = {5.0, 3.0, 2.0, 1.0, 0.5};
  protected static boolean lastShowMesh[] = new boolean [] {true, true, true, true};
  protected static boolean lastShowSurface[] = new boolean [] {true, true, true, true};
  protected static boolean lastShowSkeleton[] = new boolean [] {true, true, true, true};
  protected static boolean lastShowScene[] = new boolean [] {false, false, false, false};
  protected static int lastRenderMode[] = new int [] {ViewerCanvas.RENDER_TRANSPARENT, ViewerCanvas.RENDER_TRANSPARENT, ViewerCanvas.RENDER_TRANSPARENT, ViewerCanvas.RENDER_TRANSPARENT};

  public MeshEditorWindow(EditingWindow parent, String title, ObjectInfo obj)
  {
    super(title);
    parentWindow = parent;
    objInfo = obj.duplicate(obj.object.duplicate());
    objInfo.coords.setOrigin(new Vec3());
    objInfo.coords.setOrientation(Vec3.vz(), Vec3.vy());
    objInfo.clearDistortion();
    oldMesh = (Mesh) obj.object;
    undoStack = new UndoStack();
    if (ModellingApp.APP_ICON != null)
      setIcon(ModellingApp.APP_ICON);
    addEventLink(WindowClosingEvent.class, new Object() {
      void processEvent()
      {
        BStandardDialog dlg = new BStandardDialog("", Translate.text("saveWindowChanges"), BStandardDialog.QUESTION);
        String options[] = new String [] {
          Translate.text("saveChanges"),
          Translate.text("discardChanges"),
          Translate.text("button.cancel")
        };
        int choice = dlg.showOptionDialog(MeshEditorWindow.this, options, options[0]);
        if (choice == 0)
          doOk();
        else
          doCancel();
      }
    });
    viewPanel = new BorderContainer [4];
    viewsContainer = new GridContainer(1, 1);
    viewsContainer.setDefaultLayout(new LayoutInfo(LayoutInfo.CENTER, LayoutInfo.BOTH));
    theView = new MeshViewer [4];
    for (int i = 0; i < theView.length; i++)
    {
      viewPanel[i] = new BorderContainer();
      RowContainer controls = new RowContainer();
      viewPanel[i].add(controls, BorderContainer.NORTH);
      viewPanel[i].add(theView[i] = oldMesh.createMeshViewer(this, controls), BorderContainer.CENTER);
      theView[i].setMeshVisible(lastShowMesh[i]);
      theView[i].setSurfaceVisible(lastShowSurface[i]);
      theView[i].setSkeletonVisible(lastShowSkeleton[i]);
      theView[i].setSceneVisible(lastShowScene[i]);
      theView[i].setRenderMode(lastRenderMode[i]);
    }
    theView[1].selectOrientation(2);
    theView[2].selectOrientation(4);
    theView[3].setPerspective(true);
    theView[currentView].setDrawFocus(true);
  }
  
  /** We need to be notified of key events which take place anywhere in the window.  To make
      sure this happens, we need to add the window as a KeyPressed listener to every component
      contained in it. */
  
  protected void recursivelyAddListeners(Widget w)
  {
    if (!(w instanceof TextWidget))
      w.addEventLink(KeyPressedEvent.class, this, "keyPressed");
    if (w instanceof WidgetContainer)
      {
        Iterator children = ((WidgetContainer) w).getChildren();
        while (children.hasNext())
          recursivelyAddListeners((Widget) children.next());
      }
  }

  protected void createViewMenu()
  {
    BMenu displayMenu, coordsMenu;
    
    viewMenu = Translate.menu("view");
    menubar.add(viewMenu);
    viewMenu.add(displayMenu = Translate.menu("displayMode"));
    displayItem = new BCheckBoxMenuItem [5];
    displayMenu.add(displayItem[0] = Translate.checkboxMenuItem("wireframeDisplay", this, "displayModeChanged", theView[currentView].getRenderMode() == ViewerCanvas.RENDER_WIREFRAME));
    displayMenu.add(displayItem[1] = Translate.checkboxMenuItem("shadedDisplay", this, "displayModeChanged", theView[currentView].getRenderMode() == ViewerCanvas.RENDER_FLAT));
    displayMenu.add(displayItem[2] = Translate.checkboxMenuItem("smoothDisplay", this, "displayModeChanged", theView[currentView].getRenderMode() == ViewerCanvas.RENDER_SMOOTH));
    displayMenu.add(displayItem[3] = Translate.checkboxMenuItem("texturedDisplay", this, "displayModeChanged", theView[currentView].getRenderMode() == ViewerCanvas.RENDER_TEXTURED));
    displayMenu.add(displayItem[4] = Translate.checkboxMenuItem("transparentDisplay", this, "displayModeChanged", theView[currentView].getRenderMode() == ViewerCanvas.RENDER_TRANSPARENT));
    viewMenu.add(createShowMenu());
    viewMenu.add(coordsMenu = Translate.menu("coordinateSystem"));
    coordsItem = new BCheckBoxMenuItem [2];
    coordsMenu.add(coordsItem[0] = Translate.checkboxMenuItem("localCoords", this, "coordinateSystemChanged", theView[currentView].getUseWorldCoords() == false));
    coordsMenu.add(coordsItem[1] = Translate.checkboxMenuItem("sceneCoords", this, "coordinateSystemChanged", theView[currentView].getUseWorldCoords()));
    viewMenu.add(splitViewItem = Translate.menuItem("fourViews", this, "toggleViewsCommand"));
    viewMenu.add(Translate.menuItem("grid", this, "setGridCommand"));
    viewMenu.add(axesItem = Translate.menuItem("showCoordinateAxes", this, "showAxesCommand"));
    viewMenu.add(templateItem = Translate.menuItem("showTemplate", this, "showTemplateCommand"));
    viewMenu.add(Translate.menuItem("setTemplate", this, "setTemplateCommand"));
    if (ModellingApp.getPreferences().getObjectPreviewRenderer() != null)
    {
      viewMenu.addSeparator();
      viewMenu.add(Translate.menuItem("renderPreview", this, "renderPreviewCommand"));
    }
  }

  protected BMenu createShowMenu()
  {
    BMenu menu = Translate.menu("show");
    showItem = new BCheckBoxMenuItem [4];
    menu.add(showItem[0] = Translate.checkboxMenuItem("controlMesh", this, "shownItemChanged", theView[currentView].getMeshVisible()));
    menu.add(showItem[1] = Translate.checkboxMenuItem("surface", this, "shownItemChanged", theView[currentView].getSurfaceVisible()));
    menu.add(showItem[2] = Translate.checkboxMenuItem("skeleton", this, "shownItemChanged", theView[currentView].getSkeletonVisible()));
    menu.add(showItem[3] = Translate.checkboxMenuItem("entireScene", this, "shownItemChanged", theView[currentView].getSceneVisible()));
    return menu;
  }
  
  /** Set the Mesh object for this viewer. */
  
  public abstract void setMesh(Mesh mesh);
  
  /** Get an array of flags specifying which parts of the mesh are selected.  Depending on the selection mode,
      this may correspond to vertices, faces, edges, etc. */
  
  public abstract boolean[] getSelection();
  
  /** Set an array of flags specifying which parts of the mesh are selected.  Depending on the selection mode,
      this may correspond to vertices, faces, edges, etc. */
  
  public abstract void setSelection(boolean selected[]);

  /** Get the distance of each vertex from a selected vertex.  This is 0 for a selected
      vertex, 1 for a vertex adjacent to a selected one, etc., up to a specified maximum
      distance.  For vertices more than the maximum distance for a selected one, it is -1. */

  public abstract int[] getSelectionDistance();
  
  /** This should be called whenever the object has changed. */
  
  public void objectChanged()
  {
    getObject().clearCachedMeshes();
  }

  /* EditingWindow methods. */

  public boolean confirmClose()
  {
    return true;
  }

  public void setHelpText(String text)
  {
    helpText.setText(text);
  }
  
  public BFrame getFrame()
  {
    return this;
  }

  public void updateImage()
  {
    for (int i = 0; i < theView.length; i++)
    {
      theView[i].updateImage();
      theView[i].repaint();
    }
  }

  public void setUndoRecord(UndoRecord command)
  {
    undoStack.addRecord(command);
    updateMenus();
  }
  
  public Scene getScene()
  {
    return theView[currentView].getScene();
  }

  protected void keyPressed(KeyPressedEvent e)
  {
    int code = e.getKeyCode();

    if (e.isShiftDown() && (code == KeyPressedEvent.VK_LEFT || code == KeyPressedEvent.VK_RIGHT || code == KeyPressedEvent.VK_UP || code == KeyPressedEvent.VK_DOWN))
      tools.keyPressed(e);
    else if (code == KeyPressedEvent.VK_SPACE)
      tools.toggleDefaultTool();
    else
      currentTool.keyPressed(e, theView[currentView]);
  }
  
  public void undoCommand()
  {
    undoStack.executeUndo();
    setMesh((Mesh) getObject().object);
    updateImage();
    updateMenus();
  }
  
  public void redoCommand()
  {
    undoStack.executeRedo();
    setMesh((Mesh) getObject().object);
    updateImage();
    updateMenus();
  }

  /** Get the currently selected MeshViewer for this window. */
  
  public ViewerCanvas getView()
  {
    return theView[currentView];
  }

  /** Get all MeshViewers for this window. */
  
  public MeshViewer[] getAllViews()
  {
    return theView;
  }

  private void displayModeChanged(WidgetEvent ev)
  {
    Widget source = ev.getWidget();
    for (int i = 0; i < displayItem.length; i++)
      displayItem[i].setState(source == displayItem[i]);
    if (source == displayItem[0])
      theView[currentView].setRenderMode(lastRenderMode[currentView] = ViewerCanvas.RENDER_WIREFRAME);
    else if (source == displayItem[1])
      theView[currentView].setRenderMode(lastRenderMode[currentView] = ViewerCanvas.RENDER_FLAT);
    else if (source == displayItem[2])
      theView[currentView].setRenderMode(lastRenderMode[currentView] = ViewerCanvas.RENDER_SMOOTH);
    else if (source == displayItem[3])
      theView[currentView].setRenderMode(lastRenderMode[currentView] = ViewerCanvas.RENDER_TEXTURED);
    else if (source == displayItem[4])
      theView[currentView].setRenderMode(lastRenderMode[currentView] = ViewerCanvas.RENDER_TRANSPARENT);
    updateImage();
  }
  
  private void coordinateSystemChanged(WidgetEvent ev)
  {
    Widget source = ev.getWidget();
    for (int i = 0; i < coordsItem.length; i++)
      coordsItem[i].setState(source == coordsItem[i]);
    for (int i = 0; i < theView.length; i++)
      theView[i].setUseWorldCoords(source == coordsItem[1]);
    updateImage();
  }
  
  private void shownItemChanged(WidgetEvent ev)
  {
    Widget source = ev.getWidget();
    if (source == showItem[0])
      theView[currentView].setMeshVisible(lastShowMesh[currentView] = showItem[0].getState());
    else if (source == showItem[1])
      theView[currentView].setSurfaceVisible(lastShowSurface[currentView] = showItem[1].getState());
    else if (source == showItem[2])
      theView[currentView].setSkeletonVisible(lastShowSkeleton[currentView] = showItem[2].getState());
    else if (source == showItem[3])
      theView[currentView].setSceneVisible(lastShowScene[currentView] = showItem[3].getState());
    updateImage();
  }

  /** Allow the user to enter new coordinates for one or more vertices. */

  public void setPointsCommand()
  {
    int i, j = 0, num = 0;
    final Mesh theMesh = (Mesh) getObject().object;
    Mesh oldMesh = (Mesh) theMesh.duplicate();
    Skeleton s = theMesh.getSkeleton();
    Joint jt[] = null;
    final int selectDist[] = getSelectionDistance();
    final CoordinateSystem coords = theView[currentView].thisObjectInScene.coords;
    final MeshVertex vert[] = theMesh.getVertices();
    final Vec3 points[] = new Vec3 [vert.length];
    final ValueField xField, yField, zField;
    ValueSlider weightSlider = null;
    BComboBox jointChoice = null;
    double weight = -1.0;
    int joint = -2;
    String title;
    ComponentsDialog dlg;

    for (i = 0; i < selectDist.length; i++)
      if (selectDist[i] == 0)
      {
        num++;
        j = i;
        if (weight == -1.0)
          weight = vert[i].ikWeight;
        else if (vert[i].ikWeight != weight)
          weight = Double.NaN;
        if (joint == -2)
          joint = vert[i].ikJoint;
        else if (vert[i].ikJoint != joint)
          joint = -3;
      }
    if (num == 0)
      return;
    if (num == 1)
    {
      Vec3 pos = vert[j].r;
      if (theView[currentView].getUseWorldCoords() && coords != null)
        pos = coords.fromLocal().times(pos);
      xField = new ValueField(pos.x, ValueField.NONE, 5);
      yField = new ValueField(pos.y, ValueField.NONE, 5);
      zField = new ValueField(pos.z, ValueField.NONE, 5);
      title = Translate.text("editVertSingle");
    }
    else
    {
      xField = new ValueField(Double.NaN, ValueField.NONE, 5);
      yField = new ValueField(Double.NaN, ValueField.NONE, 5);
      zField = new ValueField(Double.NaN, ValueField.NONE, 5);
      title = Translate.text("editVertMultiple");
    }
    Object listener = new Object() {
      void processEvent()
      {
        for (int i = 0; i < selectDist.length; i++)
        {
          points[i] = vert[i].r;
          if (selectDist[i] == 0)
          {
            if (theView[currentView].getUseWorldCoords() && coords != null)
              coords.fromLocal().transform(points[i]);
            if (!Double.isNaN(xField.getValue()))
              points[i].x = xField.getValue();
            if (!Double.isNaN(yField.getValue()))
              points[i].y = yField.getValue();
            if (!Double.isNaN(zField.getValue()))
              points[i].z = zField.getValue();
            if (theView[currentView].getUseWorldCoords() && coords != null)
              coords.toLocal().transform(points[i]);
          }
        }
        theMesh.setVertexPositions(points);
        setMesh(theMesh);
        updateImage();
      }
    };
    xField.addEventLink(ValueChangedEvent.class, listener);
    yField.addEventLink(ValueChangedEvent.class, listener);
    zField.addEventLink(ValueChangedEvent.class, listener);
    if (s == null)
      dlg = new ComponentsDialog(this, title, new Widget [] {xField, yField, zField},
        new String [] {"X", "Y", "Z"});
    else
    {
      weightSlider = new ValueSlider(0.0, 1.0, 100, weight);
      jointChoice = new BComboBox();
      jointChoice.add(Translate.text("none"));
      jt = s.getJoints();
      for (i = 0; i < jt.length; i++)
        jointChoice.add(jt[i].name);
      if (joint == -3)
        jointChoice.add("");
      jointChoice.setSelectedIndex(0);
      for (i = 0; i < jt.length; i++)
        if (jt[i].id == joint)
          jointChoice.setSelectedIndex(i+1);
      if (joint == -3)
        jointChoice.setSelectedIndex(jt.length+1);
      dlg = new ComponentsDialog(this, title, new Widget [] {xField, yField, zField, jointChoice, weightSlider},
        new String [] {"X", "Y", "Z", Translate.text("ikBone"), Translate.text("ikWeight")});
    }
    if (dlg.clickedOk())
    {
      for (i = 0; i < selectDist.length; i++)
        if (selectDist[i] == 0)
        {
          if (weightSlider != null && !Double.isNaN(weightSlider.getValue()))
            vert[i].ikWeight = weightSlider.getValue();
          if (jointChoice != null)
          {
            if (jointChoice.getSelectedIndex() == 0)
              vert[i].ikJoint = -1;
            else if (jointChoice.getSelectedIndex() <= jt.length)
              vert[i].ikJoint = jt[jointChoice.getSelectedIndex()-1].id;
          }
        }
      setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, oldMesh}));
    }
    else
    {
      theMesh.copyObject((Object3D) oldMesh);
      setMesh(theMesh);
      updateImage();
    }
  }
  
  /** Allow the user to transform one or more vertices. */

  public void transformPointsCommand()
  {
    int i, j;
    Mesh theMesh = (Mesh) getObject().object;
    int selectDist[] = getSelectionDistance();
    MeshVertex vert[] = theMesh.getVertices();
    Vec3 points[] = new Vec3 [vert.length], center;
    CoordinateSystem coords = theView[currentView].thisObjectInScene.coords;

    for (i = 0; i < selectDist.length && selectDist[i] == -1; i++);
    if (i == selectDist.length)
      return;
    
    // Create the dialog.
    
    FormContainer content = new FormContainer(4, 5);
    LayoutInfo eastLayout = new LayoutInfo(LayoutInfo.EAST, LayoutInfo.NONE, new Insets(0, 0, 0, 5), null);
    content.add(Translate.label("Move"), 0, 1, eastLayout);
    content.add(Translate.label("Rotate"), 0, 2, eastLayout);
    content.add(Translate.label("Scale"), 0, 3, eastLayout);
    content.add(new BLabel("X"), 1, 0);
    content.add(new BLabel("Y"), 2, 0);
    content.add(new BLabel("Z"), 3, 0);
    ValueField fields[] = new ValueField[9];
    for (i = 0; i < 9; i++)
      content.add(fields[i] = new ValueField(Double.NaN, ValueField.NONE), (i%3)+1, (i/3)+1);
    RowContainer row = new RowContainer();
    content.add(row, 0, 4, 4, 1);
    row.add(Translate.label("transformAround"));
    BComboBox centerChoice = new BComboBox(new String [] {
      Translate.text("centerOfSelection"),
      Translate.text("objectOrigin")
    });
    row.add(centerChoice);
    PanelDialog dlg = new PanelDialog(this, Translate.text("transformPoints"), content);
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    double val[] = new double [9];
    for (i = 0; i < val.length; i++)
    {
      val[i] = fields[i].getValue();
      if (Double.isNaN(val[i]))
        val[i] = (i < 6 ? 0.0 : 1.0);
    }
    Mat4 m = Mat4.translation(val[0], val[1], val[2]);
    m = m.times(Mat4.xrotation(val[3]*Math.PI/180.0));
    m = m.times(Mat4.yrotation(val[4]*Math.PI/180.0));
    m = m.times(Mat4.zrotation(val[5]*Math.PI/180.0));
    m = m.times(Mat4.scale(val[6], val[7], val[8]));
    if (theView[currentView].getUseWorldCoords() && coords != null)
      m = coords.toLocal().times(m).times(coords.fromLocal());
    if (centerChoice.getSelectedIndex() == 0)
    {
      center = new Vec3();
      for (i = j = 0; i < selectDist.length; i++)
        if (selectDist[i] == 0)
        {
          center.add(vert[i].r);
          j++;
        }
      center.scale(1.0/j);
      m = Mat4.translation(center.x, center.y, center.z).times(m).times(Mat4.translation(-center.x, -center.y, -center.z));
    }
    for (i = 0; i < selectDist.length; i++)
    {
      points[i] = vert[i].r;
      if (selectDist[i] == 0)
        points[i] = m.times(points[i]);
    }
    theMesh.setVertexPositions(points);
    setMesh(theMesh);
    updateImage();
  }
  
  /** Displace selected vertices by a random amount. */
  
  public void randomizeCommand()
  {
    int i;
    Mesh theMesh = (Mesh) getObject().object;
    int selectDist[] = getSelectionDistance();
    CoordinateSystem coords = theView[currentView].thisObjectInScene.coords;
    MeshVertex vert[] = theMesh.getVertices();
    Vec3 points[] = new Vec3 [vert.length];
    ValueField xfield, yfield, zfield;

    for (i = 0; i < selectDist.length && selectDist[i] == -1; i++);
    if (i == selectDist.length)
      return;
    xfield = new ValueField(0.0, ValueField.NONE);
    yfield = new ValueField(0.0, ValueField.NONE);
    zfield = new ValueField(0.0, ValueField.NONE);
    ComponentsDialog dlg = new ComponentsDialog(this, "Maximum random displacement:", new Widget []
    		{xfield, yfield, zfield}, new String[] {"X", "Y", "Z"});
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    for (i = 0; i < selectDist.length; i++)
    {
      points[i] = vert[i].r;
      if (selectDist[i] == 0)
      {
        if (theView[currentView].getUseWorldCoords() && coords != null)
          coords.fromLocal().transform(points[i]);
        points[i].x += (1.0-2.0*Math.random())*xfield.getValue();
        points[i].y += (1.0-2.0*Math.random())*yfield.getValue();
        points[i].z += (1.0-2.0*Math.random())*zfield.getValue();
        if (theView[currentView].getUseWorldCoords() && coords != null)
          coords.toLocal().transform(points[i]);
      }
    }
    theMesh.setVertexPositions(points);
    setMesh(theMesh);
    updateImage();
  }
  
  /* Center the mesh. */
  
  public void centerCommand()
  {
    Mesh theMesh = (Mesh) getObject().object;
    MeshVertex vert[] = theMesh.getVertices();
    CoordinateSystem coords = theView[currentView].thisObjectInScene.coords;
    Vec3 center = theMesh.getBounds().getCenter(), points[] = new Vec3 [vert.length];

    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_VERTEX_POSITIONS, new Object [] {theMesh, theMesh.getVertexPositions()}));
    if (theView[currentView].getUseWorldCoords() && coords != null)
    {
      coords.fromLocal().transform(center);
      coords.toLocal().transformDirection(center);
    }
    for (int i = 0; i < vert.length; i++)
      points[i] = vert[i].r.minus(center);
    theMesh.setVertexPositions(points);
    Skeleton skeleton = theMesh.getSkeleton();
    if (skeleton != null)
    {
      Joint joint[] = skeleton.getJoints();
      for (int i = 0; i < joint.length; i++)
        joint[i].coords.setOrigin(joint[i].coords.getOrigin().minus(center));
    }
    setMesh(theMesh);
    updateImage();
  }
  
  /** Allow the user to set the mesh tension. */
  
  public void setTensionCommand()
  {
    ValueField distanceField = new ValueField((double) tensionDistance, ValueField.NONNEGATIVE+ValueField.INTEGER);
    BComboBox tensionChoice = new BComboBox(new String [] {
      Translate.text("VeryLow"),
      Translate.text("Low"),
      Translate.text("Medium"),
      Translate.text("High"),
      Translate.text("VeryHigh")
    });
    tensionChoice.setSelectedIndex(meshTension);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("setTensionTitle"), 
		new Widget [] {distanceField, tensionChoice}, 
		new String [] {Translate.text("maxDistance"), Translate.text("Tension")});
    if (!dlg.clickedOk())
      return;
    tensionDistance = (int) distanceField.getValue();
    meshTension = tensionChoice.getSelectedIndex();
  }

  public static double getMeshTension()
  {
    return tensionArray[meshTension];
  }
  
  public static int getTensionDistance()
  {
    return tensionDistance;
  }
  
  /** Allow the user to set the texture parameters for selected vertices. */
  
  public void setParametersCommand()
  {
    ObjectInfo info = getObject();
    Mesh theMesh = (Mesh) info.object;
    final MeshVertex vert[] = theMesh.getVertices();
    TextureParameter param[] = info.object.getParameters();
    final ParameterValue paramValue[] = info.object.getParameterValues();
    int i, j, k, paramIndex[] = null;
    final int selectDist[] = getSelectionDistance();
    double value[];
    String label[];

    for (j = 0; j < selectDist.length && selectDist[j] != 0; j++);
    if (j == selectDist.length)
      return;
    if (param != null)
    {
      // Find the list of per-vertex parameters.
      
      int num = 0;
      for (i = 0; i < param.length; i++)
        if (paramValue[i] instanceof VertexParameterValue)
          num++;
      paramIndex = new int [num];
      for (i = 0, k = 0; k < param.length; k++)
        if (paramValue[k] instanceof VertexParameterValue)
          paramIndex[i++] = k;
    }
    if (paramIndex == null || paramIndex.length == 0)
    {
      new BStandardDialog("", Translate.text("noPerVertexParams"), BStandardDialog.INFORMATION).showMessageDialog(this);
      return;
    }
    value = new double [paramIndex.length];
    for (i = 0; i < paramIndex.length; i++)
    {
      double currentVal[] = ((VertexParameterValue) paramValue[paramIndex[i]]).getValue();
      value[i] = currentVal[j];
      for (k = j; k < selectDist.length; k++)
        if (selectDist[k] == 0 && currentVal[k] != value[i])
          value[i] = Double.NaN;
    }
    
    // Define an inner class used for resetting parameters that represent texture coordinates.
    
    class ResetButton extends BButton
    {
      VertexParameterValue xparamVal, yparamVal, zparamVal;
      double xvalList[], yvalList[], zvalList[];
      ValueField xfield, yfield, zfield;
      
      public ResetButton()
      {
	super(Translate.text("Reset"));
	addEventLink(CommandEvent.class, this);
      }
      
      public void addParam(int index, int type, ValueField field)
      {
	if (type == TextureParameter.X_COORDINATE)
        {
          xparamVal = (VertexParameterValue) paramValue[index];
          xvalList = xparamVal.getValue();
          xfield = field;
        }
	else if (type == TextureParameter.Y_COORDINATE)
        {
          yparamVal = (VertexParameterValue) paramValue[index];
          yvalList = yparamVal.getValue();
          yfield = field;
        }
	else if (type == TextureParameter.Z_COORDINATE)
        {
          zparamVal = (VertexParameterValue) paramValue[index];
          zvalList = zparamVal.getValue();
          zfield = field;
        }
      }
      
      private void processEvent()
      {
        BStandardDialog dlg = new BStandardDialog("", Translate.text("resetCoordsToPos"), BStandardDialog.QUESTION);
        String options[] = new String [] {Translate.text("button.ok"), Translate.text("button.cancel")};
        int choice = dlg.showOptionDialog(this, options, options[0]);
	if (choice == 1)
	  return;
	double xval = Double.NaN, yval = Double.NaN, zval = Double.NaN;
	for (int ind = 0; ind < selectDist.length; ind++)
	  if (selectDist[ind] == 0)
          {
            // Reset the texture coordinates for this vertex.
            
            if (xparamVal != null)
            {
              xvalList[ind] = vert[ind].r.x;
              if (Double.isNaN(xval))
              {
                xval = vert[ind].r.x;
                xfield.setValue(xval);
              }
              else if (xval != vert[ind].r.x)
                xfield.setValue(Double.NaN);
            }
            if (yparamVal != null)
            {
              yvalList[ind] = vert[ind].r.y;
              if (Double.isNaN(yval))
              {
                yval = vert[ind].r.y;
                yfield.setValue(yval);
              }
              else if (yval != vert[ind].r.y)
                yfield.setValue(Double.NaN);
            }
            if (zparamVal != null)
            {
              zvalList[ind] = vert[ind].r.z;
              if (Double.isNaN(zval))
              {
                zval = vert[ind].r.z;
                zfield.setValue(zval);
              }
              else if (zval != vert[ind].r.z)
                zfield.setValue(Double.NaN);
            }
          }
        if (xparamVal != null)
          xparamVal.setValue(xvalList);
        if (yparamVal != null)
          yparamVal.setValue(yvalList);
        if (zparamVal != null)
          zparamVal.setValue(zvalList);
      }
    }
    
    // Build the panel for editing the values.
    
    Widget editWidget[] = new Widget [paramIndex.length];
    ColumnContainer content = new ColumnContainer();
    content.setDefaultLayout(new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, null, null));
    LayoutInfo indent1 = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, new Insets(0, 10, 0, 0), null);
    LayoutInfo indent2 = new LayoutInfo(LayoutInfo.WEST, LayoutInfo.NONE, new Insets(0, 20, 0, 0), null);
    RowContainer coordsPanel = null;
    ResetButton reset = null;
    Object lastOwner = null;
    if (info.object.getTexture() instanceof LayeredTexture)
    {
      // This is a layered texture, so we want to group the parameters by layer.
      
      LayeredMapping map = (LayeredMapping) info.object.getTextureMapping();
      Texture layer[] = map.getLayers();
      for (k = 0; k < layer.length; k++)
      {
        coordsPanel = null;
        content.add(new BLabel(Translate.text("layerLabel", Integer.toString(k+1), layer[k].getName())));
        TextureParameter layerParam[] = map.getLayerParameters(k);
        boolean any = false;
        for (i = 0; i < paramIndex.length; i++)
        {
          // Determine whether this parameter is actually part of this layer.
          
          int m;
          TextureParameter pm = param[paramIndex[i]];
          for (m = 0; m < layerParam.length; m++)
            if (layerParam[m].equals(pm))
              break;
          if (m == layerParam.length)
            continue;
          any = true;
          
          // It is, so add it.

          editWidget[i] = pm.getEditingWidget(value[i]);
          if (pm.type == TextureParameter.NORMAL_PARAMETER)
          {
            RowContainer row = new RowContainer();
            row.add(new BLabel(pm.name));
            row.add(editWidget[i]);
            content.add(row, indent1);
            if (coordsPanel != null)
              coordsPanel.add(reset);
            coordsPanel = null;
            reset = null;
          }
          else if (coordsPanel == null)
          {
            coordsPanel = new RowContainer();
            content.add(Translate.label("texMappingCoords"), indent1);
            content.add(coordsPanel, indent2);
            coordsPanel.add(new BLabel(pm.name));
            coordsPanel.add(editWidget[i]);
            reset = new ResetButton();
            reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
          }
          else
          {
            coordsPanel.add(new BLabel(pm.name));
            coordsPanel.add(editWidget[i]);
            reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
          }
        }
        if (coordsPanel != null)
          coordsPanel.add(reset);
        if (!any)
          content.add(Translate.label("noLayerPerVertexParams"), indent1);
      }
    }
    else
    {
      // This is a simple texture, so just list off all the parameters.
      
      content.add(new BLabel(Translate.text("Texture")+": "+info.object.getTexture().getName()));
      for (i = 0; i < paramIndex.length; i++)
      {
        TextureParameter pm = param[paramIndex[i]];
        editWidget[i] = pm.getEditingWidget(value[i]);
        if (pm.type == TextureParameter.NORMAL_PARAMETER)
        {
          RowContainer row = new RowContainer();
          row.add(new BLabel(pm.name));
          row.add(editWidget[i]);
          content.add(row, indent1);
          if (coordsPanel != null)
            coordsPanel.add(reset);
          coordsPanel = null;
          coordsPanel = null;
        }
        else if (coordsPanel == null)
        {
          coordsPanel = new RowContainer();
          content.add(Translate.label("texMappingCoords"), indent1);
          content.add(coordsPanel, indent2);
          coordsPanel.add(new BLabel(pm.name));
          coordsPanel.add(editWidget[i]);
          reset = new ResetButton();
          reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
        }
        else
        {
          coordsPanel.add(new BLabel(pm.name));
          coordsPanel.add(editWidget[i]);
          reset.addParam(paramIndex[i], pm.type, (ValueField) editWidget[i]);
        }
      }
      if (coordsPanel != null)
        coordsPanel.add(reset);
    }
    PanelDialog dlg = new PanelDialog(this, Translate.text("texParamsForSelectedPoints"), content);
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    for (j = 0; j < editWidget.length; j++)
    {
      double d;
      if (editWidget[j] instanceof ValueField)
        d = ((ValueField) editWidget[j]).getValue();
      else
        d = ((ValueSlider) editWidget[j]).getValue();
      if (!Double.isNaN(d))
      {
        double val[] = ((VertexParameterValue) paramValue[paramIndex[j]]).getValue();
        for (i = 0; i < selectDist.length; i++)
          if (selectDist[i] == 0)
            val[i] = d;
        ((VertexParameterValue) paramValue[paramIndex[j]]).setValue(val);
      }
    }
  }
  
  /** Toggle whether the coordinate axes are shown. */
  
  public void showAxesCommand()
  {
    boolean wasShown = theView[currentView].getShowAxes();
    axesItem.setText(Translate.text(wasShown ? "menu.showCoordinateAxes" : "menu.hideCoordinateAxes"));
    for (int i = 0; i < theView.length; i++)
      theView[i].setShowAxes(!wasShown);
    updateImage();
  }
  
  /** Toggle whether the template is shown. */
  
  public void showTemplateCommand()
  {
    boolean wasShown = theView[currentView].getTemplateShown();
    templateItem.setText(Translate.text(wasShown ? "menu.showTemplate" : "menu.hideTemplate"));
    theView[currentView].setShowTemplate(!wasShown);
    updateImage();
  }
  
  /** Toggle whether there are one or four views shown. */
  
  public void toggleViewsCommand()
  {
    if (numViewsShown == 4)
    {
      numViewsShown = 1;
      viewsContainer.setRowCount(1);
      viewsContainer.setColumnCount(1);
      viewsContainer.add(viewPanel[currentView], 0, 0);
      splitViewItem.setText(Translate.text("menu.fourViews"));
    }
    else
    {
      numViewsShown = 4;
      viewsContainer.setRowCount(2);
      viewsContainer.setColumnCount(2);
      viewsContainer.add(viewPanel[0], 0, 0);
      viewsContainer.add(viewPanel[1], 1, 0);
      viewsContainer.add(viewPanel[2], 0, 1);
      viewsContainer.add(viewPanel[3], 1, 1);
      splitViewItem.setText(Translate.text("menu.oneView"));
    }
    viewsContainer.layoutChildren();
    updateImage();
    viewPanel[currentView].requestFocus();
  }
  
  /** Allow the user to set the template image. */
  
  public void setTemplateCommand()
  {
    BFileChooser fc = new BFileChooser(BFileChooser.OPEN_FILE, Translate.text("selectTemplateImage"));
    if (!fc.showDialog(this))
      return;
    try
    {
      theView[currentView].setTemplateImage(fc.getSelectedFile());
    }
    catch (InterruptedException ex)
    {
      new BStandardDialog("", Translate.text("errorLoadingImage", fc.getSelectedFile().getName()), BStandardDialog.ERROR).showMessageDialog(this);
      return;
    }
    templateItem.setEnabled(true);
    templateItem.setText(Translate.text("menu.hideTemplate"));
    theView[currentView].setShowTemplate(true);
    updateImage();
  }

  /** Delete the select joint from the skeleton. */
  
  public void deleteJointCommand()
  {
    Mesh theMesh = (Mesh) getObject().object;
    Skeleton s = theMesh.getSkeleton();
    
    if (s == null)
      return;
    Joint j = s.getJoint(theView[currentView].getSelectedJoint());
    if (j == null)
      return;
    String options[] = new String [] {Translate.text("Yes"), Translate.text("No")};
    BStandardDialog dlg = new BStandardDialog("", Translate.text(j.children.length == 0 ? "deleteBone" : "deleteBoneAndChildren", j.name), BStandardDialog.QUESTION);
    if (dlg.showOptionDialog(this, options, options[1]) == 1)
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    s.deleteJoint(theView[currentView].getSelectedJoint());
    for (int i = 0; i < theView.length; i++)
      theView[i].setSelectedJoint(j.parent == null ? -1 : j.parent.id);
    updateImage();
    updateMenus();
  }
  
  /** Allow the user to set the parent of the selected joint. */
  
  public void setJointParentCommand()
  {
    Mesh theMesh = (Mesh) getObject().object;
    Skeleton s = theMesh.getSkeleton();
    
    if (s == null)
      return;
    Joint j = s.getJoint(theView[currentView].getSelectedJoint());
    if (j == null)
      return;
    
    // Make a list of all joints which are possibilities to be the parent.
    
    Joint joint[] = s.getJoints();
    boolean isChild[] = new boolean [joint.length];
    markChildJoints(s, j, isChild);
    Vector options = new Vector();
    for (int i = 0; i < isChild.length; i++)
      if (!isChild[i])
        options.addElement(joint[i]);
    
    // Display a window for the user to select the parent joint.
    
    BList ls = new BList();
    ls.setMultipleSelectionEnabled(false);
    ls.add("("+Translate.text("None")+")");
    ls.setSelected(0, true);
    for (int i = 0; i < options.size(); i++)
    {
      ls.add(((Joint) options.elementAt(i)).name);
      if (options.elementAt(i) == j.parent)
        ls.setSelected(i+1, true);
    }
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("selectParentBone", j.name),
        new Widget [] {UIUtilities.createScrollingList(ls)}, new String [] {null});
    if (!dlg.clickedOk() || ls.getSelectedIndex() == -1)
      return;
    
    // Set the parent.
    
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    Joint oldParent = j.parent;
    if (ls.getSelectedIndex() == 0)
      s.setJointParent(j, null);
    else
      s.setJointParent(j, (Joint) options.elementAt(ls.getSelectedIndex()-1));
    
    // Adjust the coordinate system.
    
    if (j.parent != null)
    {
      Vec3 oldZdir = j.coords.getZDirection();
      Vec3 oldYdir = j.coords.getUpDirection();
      Vec3 oldXdir = oldYdir.cross(oldZdir);
      Vec3 xdir, ydir, zdir;
      zdir = j.coords.getOrigin().minus(j.parent.coords.getOrigin());
      j.length.pos = zdir.length();
      zdir.normalize();
      if (Math.abs(oldXdir.dot(zdir)) < Math.abs(oldYdir.dot(zdir)))
      {
        xdir = oldXdir.minus(zdir.times(oldXdir.dot(zdir)));
        xdir.normalize();
        ydir = zdir.cross(xdir);
      }
      else
      {
        ydir = oldYdir.minus(zdir.times(oldYdir.dot(zdir)));
        ydir.normalize();
        xdir = ydir.cross(zdir);
      }
      j.coords.setOrientation(zdir, ydir);
      j.calcAnglesFromCoords(false);
      for (int i = 0; i < j.children.length; i++)
        j.children[i].calcAnglesFromCoords(false);
    }
    else
      j.calcAnglesFromCoords(false);
    updateImage();
    updateMenus();
  }
  
  /** This is called by setJointParentCommand().  It identifies joints which are children of the
      specified one. */
  
  private void markChildJoints(Skeleton s, Joint j, boolean isChild[])
  {
    isChild[s.findJointIndex(j.id)] = true;
    for (int i = 0; i < j.children.length; i++)
      markChildJoints(s, j.children[i], isChild);
  }
  
  /** Allow the user to edit the selected joint. */

  public void editJointCommand()
  {
    Mesh theMesh = (Mesh) getObject().object;
    Skeleton s = theMesh.getSkeleton();
    
    if (s == null)
      return;
    Joint j = s.getJoint(theView[currentView].getSelectedJoint());
    if (j == null)
      return;
    new JointEditorDialog(this, j.id);
    updateImage();
    updateMenus();
  }
  
  /** Present a window for binding the selected vertices to the skeleton. */
  
  public void bindSkeletonCommand()
  {
    Mesh theMesh = (Mesh) getObject().object;
    Skeleton s = theMesh.getSkeleton();
    if (s == null)
      return;
    
    // Find the selected vertices.
    
    int i, j, selected[] = getSelectionDistance();
    for (j = 0; j < selected.length && selected[j] != 0; j++);
    if (j == selected.length)
      return;
    
    // Prompt the user.
    
    ValueSlider blendSlider = new ValueSlider(0.0, 1.0, 100, 0.5);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("bindPointsToSkeleton"),
      new Widget [] {blendSlider}, new String [] {Translate.text("ikWeightBlending")});
    if (!dlg.clickedOk())
      return;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_OBJECT, new Object [] {theMesh, theMesh.duplicate()}));
    double blend = blendSlider.getValue();
    
    // Find the position and axis vectors for each joint.
    
    Joint joint[] = s.getJoints();
    Vec3 pos[] = new Vec3 [joint.length], axis[] = new Vec3 [joint.length];
    for (i = 0; i < joint.length; i++)
    {
      pos[i] = joint[i].coords.getOrigin();
      if (joint[i].parent == null || joint[i].length.pos == 0.0)
        continue;
      axis[i] = joint[i].coords.getZDirection();
      axis[i] = axis[i].times(1.0/axis[i].length());
    }
    
    // Loop over vertices and decide which joint to bind each one to.
    
    MeshVertex vert[] = theMesh.getVertices();
    double dist[] = new double [joint.length];
    for (i = 0; i < selected.length; i++)
    {
      if (selected[i] != 0)
        continue;
      int nearest = -1;
      
      // Find which bone it is nearest to.
      
      for (j = 0; j < joint.length; j++)
      {
        dist[j] = distToBone(vert[i].r, joint[j], axis[j]);
        if (nearest == -1 || dist[j] < dist[nearest])
          nearest = j;
      }
      if (nearest == -1)
        continue;
      
      // Find the secondary bone.
      
      int second = -1;
      if (joint[nearest].parent != null)
        second = s.findJointIndex(joint[nearest].parent.id);
      for (j = 0; j < joint[nearest].children.length; j++)
      {
        int k = s.findJointIndex(joint[nearest].children[j].id);
        if (k != -1 && (second == -1 || dist[k] < dist[second]))
          second = k;
      }

      // Select the binding parameters.
      
      if (second == -1)
      {
        vert[i].ikJoint = joint[nearest].id;
        vert[i].ikWeight = 1.0;
      }
      else if (joint[nearest].parent != null && joint[second].id == joint[nearest].parent.id)
      {
        vert[i].ikJoint = joint[nearest].id;
        double ratio = dist[nearest]/dist[second];
        if (ratio <= 1.0-blend)
          vert[i].ikWeight = 1.0;
        else
          vert[i].ikWeight = 0.5+0.5*(1.0-ratio)/blend;
      }
      else
      {
        double ratio = dist[nearest]/dist[second];
        if (ratio <= 1.0-blend)
          {
            vert[i].ikJoint = joint[nearest].id;
            vert[i].ikWeight = 1.0;
          }
        else
          {
            vert[i].ikJoint = joint[second].id;
            vert[i].ikWeight = 0.5-0.5*(1.0-ratio)/blend;
          }
      }
      vert[i].ikWeight = 0.001*Math.round(vert[i].ikWeight*1000.0);
    }
  }
  
  /* Calculate the distance between a vertex and a bone. */
  
  private double distToBone(Vec3 v, Joint j, Vec3 axis)
  {
    Vec3 end = j.coords.getOrigin();
    if (axis == null)
      return end.distance(v);
    Vec3 base = j.parent.coords.getOrigin();
    Vec3 diff = v.minus(base);
    double dot = diff.dot(axis);
    if (dot < 0.0)
      return base.distance(v);
    if (dot > j.length.pos)
      return end.distance(v);
    diff.subtract(axis.times(dot));
    return diff.length();
  }
  
  /** Display a window for importing a skeleton from another object. */
  
  protected void importSkeletonCommand()
  {
    final TreeList tree = new TreeList(this);
    tree.setPreferredSize(new Dimension(130, 100));
    tree.setAllowMultiple(false);
    tree.setUpdateEnabled(false);
    Scene theScene = getScene();
    class TreeElem extends ObjectTreeElement
    {
      public TreeElem(ObjectInfo info, TreeElement parent, TreeList tree)
      {
        super(info, parent, tree, false);
        selectable = (info != theView[currentView].thisObjectInScene && info.getSkeleton() != null);
        for (int i = 0; i < info.children.length; i++)
          children.addElement(new TreeElem(info.children[i], this, tree));
      }
      public boolean isGray()
      {
        return !selectable;
      }
      public boolean canAcceptAsParent(TreeElement el)
      {
        return false;
      }
    };
    for (int i = 0; i < theScene.getNumObjects(); i++)
    {
      ObjectInfo info = theScene.getObject(i);
      if (info.parent == null)
        tree.addElement(new TreeElem(info, null, tree));
    }
    tree.setUpdateEnabled(true);
    tree.setBackground(Color.white);
    BScrollPane sp = new BScrollPane(tree, BScrollPane.SCROLLBAR_ALWAYS, BScrollPane.SCROLLBAR_ALWAYS);
    sp.getVerticalScrollBar().setUnitIncrement(10);
    sp.setForceWidth(true);
    sp.setForceHeight(true);
    ComponentsDialog dlg = new ComponentsDialog(this, "selectImportSkeleton", new Widget [] {sp}, new String [] {null});
    if (!dlg.clickedOk() || tree.getSelectedObjects().length == 0)
      return;
    Mesh theMesh = (Mesh) getObject().object;
    setUndoRecord(new UndoRecord(this, false, UndoRecord.COPY_SKELETON, new Object [] {theMesh.getSkeleton(), theMesh.getSkeleton().duplicate()}));
    ObjectInfo info = (ObjectInfo) tree.getSelectedObjects()[0];
    theMesh.getSkeleton().addAllJoints(info.object.getSkeleton());
    updateImage();
    updateMenus();
  }
  
  /** Set the grid options for the current window. */
  
  public void setGridCommand()
  {
    ValueField spaceField = new ValueField(theView[currentView].gridSpacing, ValueField.POSITIVE);
    ValueField divField = new ValueField(theView[currentView].gridSubdivisions, ValueField.POSITIVE+ValueField.INTEGER);
    BCheckBox showBox = new BCheckBox(Translate.text("showGrid"), theView[currentView].showGrid);
    BCheckBox snapBox = new BCheckBox(Translate.text("snapToGrid"), theView[currentView].snapToGrid);
    ComponentsDialog dlg = new ComponentsDialog(this, Translate.text("gridTitle"), 
		new Widget [] {spaceField, divField, showBox, snapBox}, 
		new String [] {Translate.text("gridSpacing"), Translate.text("snapToSubdivisions"), null, null});
    if (!dlg.clickedOk())
      return;
    for (int i = 0; i < theView.length; i++)
    {
      theView[i].gridSpacing = spaceField.getValue();
      theView[i].gridSubdivisions = (int) divField.getValue();
      theView[i].showGrid = showBox.getState();
      theView[i].snapToGrid = snapBox.getState();
      theView[i].setGrid(spaceField.getValue(), (int) divField.getValue(), showBox.getState(), snapBox.getState());
    }
    updateImage();
  }
  
  /** Render a preview of the mesh. */
  
  public void renderPreviewCommand()
  {
    theView[currentView].previewObject();
  }

  /** Given a list of deltas which will be added to the selected vertices, calculate the
      corresponding deltas for the unselected vertices according to the mesh tension. */
  
  public abstract void adjustDeltas(Vec3 delta[]);
  
  /** This will be called when the user clicks the OK button. */
  
  protected abstract void doOk();
  
  /** This will be called when the user clicks the Cancel button. */
  
  protected abstract void doCancel();
}